package colecciones.colecciones;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import colecciones.bloques.BloqueExec;
import colecciones.bloques.BloqueFn;
import colecciones.bloques.BloqueFold;
import colecciones.bloques.BloqueTest;

/**
 * Objeto que se comporta m�s similar a las colecciones de Smalltalk o a los arrays de JavaScript que a las horribles colecciones de Java.
 * @author Gon
 */
public class MiColeccion<T> implements Iterable<T> {
	private ArrayList<T> col=new ArrayList<T>();
	private int index=0;
	

	/**
	 * Permite que la colecci�n sea iterable.
	 */
	@Override
	public Iterator<T> iterator() {
		return this.col.iterator();
	}
	
	/////////////////////////////////////////////////
	// M�todos de array
	/////////////////////////////////////////////////
	/**
	 * Devuelve el elemento que est� en la posici�n pedida.
	 * @param index
	 * @return unElemento
	 */
	public T get(int index) {
		return this.col.get(index);
	}
	
	/**
	 * Agrega un elemento a la colecci�n.
	 * Admite chaining.
	 * @param unElemento
	 * @return laColeccion
	 */
	public MiColeccion<T> add(T unElemento) {
		this.col.add(unElemento);
		return this;
	}
	
	/**
	 * Agrega varios elementos de una (los recibe como Colection) a la colecci�n.
	 * Admite chaining.
	 * @param elementos
	 * @return laColeccion
	 */
	public MiColeccion<T> addAll(Collection<? extends T> elementos) {
		this.col.addAll(elementos);
		return this;
	}
	
	/**
	 * Quita un elemento de la colecci�n.
	 * Admite chaining.
	 * @param unElemento
	 * @return laColeccion
	 */
	public MiColeccion<T> remove(Object unElemento) {
		this.col.remove(unElemento);
		return this;
	}
	
	/**
	 * Quita varios elementos de una (los recibe como Colection) de la colecci�n.
	 * Admite chaining.
	 * @param elementos
	 * @return laColeccion
	 */
	public MiColeccion<T> removeAll(Collection<?> elementos) {
		this.col.removeAll(elementos);
		return this;
	}
	
	/**
	 * Obtener el tama�o de la colecci�n.
	 * @return size
	 */
	public int size() {
		return this.col.size();
	}
	
	/**
	 * Saber si la colecci�n est� vac�a o no.
	 * @return isEmpty
	 */
	public boolean isEmpty() {
		return this.col.isEmpty();
	}
	
	/**
	 * Saber si la colecci�n cotiene o no un elemento
	 * @param unElemento
	 * @return contieneONoAlElemento
	 */
	public boolean includes(T unElemento) {
		return this.col.contains(unElemento);
	}
	
	/**
	 * Saber si la colecci�n contiene o no todos los elementos pasados
	 * @param elementos
	 * @return contieneONoTodosLosElementos
	 */
	public boolean includesAll(Collection<T> elementos) {
		return this.col.containsAll(elementos);
	}
	
	/////////////////////////////////////////////////
	// M�todos de copia.
	/////////////////////////////////////////////////
	/**
	 * Devuelve el array que est� wrappeando.
	 * @return elArrayWrappeado
	 */
	public ArrayList<T> asArrayList() {
		return this.col;
	}
	
	/**
	 * Devuelve una nueva colecci�n (MiColecci�n) con los mismo elementos de �sta.
	 * @return unaCopiaDeLaColeccion
	 */
	public MiColeccion<T> copy() {
		return new MiColeccion<T>().addAll(this.asArrayList());
	}
	
	/**
	 * Devuelve una nueva colecci�n, con los mismos elementos que la original, pero en el orden inverso.
	 * @return laColeccionDadaVuelta
	 */
	public MiColeccion<T> flip() {
		if(this.size()==0) {
			return new MiColeccion<T>();
		}
		
		MiColeccion<T> ret=new MiColeccion<T>();
		T unElemento;
		int tempPosPointerAnt=this.getPointerPosition();
		
		this.setPointerPosition(this.size() - 1);
		while((unElemento=this.prev())!=null) {
			ret.add(unElemento);
		}
		
		this.setPointerPosition(tempPosPointerAnt);
		return ret;
	}
	
	/////////////////////////////////////////////////
	// M�todos de iteraci�n tranquilos.
	/////////////////////////////////////////////////
	/**
	 * Resetea el puntero de iteraci�n.
	 * Admite chaining.
	 * @return laColeccion
	 */
	public MiColeccion<T> reset() {
		this.index=0;
		return this;
	}
	
	/**
	 * Setear el puntero interno manualmente.
	 * Admite chaining.
	 * @param pos
	 * @return laColeccion
	 * @throws PunteroIncorrectoDeMiColeccionException si el puntero no es v�lido.
	 */
	public MiColeccion<T> setPointerPosition(int pos) {
		if(pos >= this.size() || pos < 0) {
			throw new PunteroIncorrectoDeMiColeccionException();
		}
		this.index=pos;
		return this;
	}
	
	/**
	 * Devuelve la posici�n del punter de iteraci�n.
	 * @return int index
	 */
	public int getPointerPosition() {
		return this.index;
	}
	
	/**
	 * Saber si el puntero de iteraci�n lleg� al final de la colecci�n.
	 * @return hasNext
	 */
	public boolean hasNext() {
		return this.size() != this.index;
	}
	
	/**
	 * Saber si el puntero de iteraci�n est� al comienzo de la colecci�n.
	 * @return isFirst
	 */
	public boolean isFirst() {
		return this.index == 0;
	}
	
	/**
	 * Devolver el elemento actualmente apuntado por el puntero de iteraci�n.
	 * @return unElemento
	 */
	public T current() {
		return this.get(this.index);
	}
	
	/**
	 * Avanzar una posici�n en la colecci�n y devolver el elemento actual.
	 * @return unElemento
	 */
	public T next() {
		if(this.hasNext()) {
			T el=this.current();
			this.index += 1;
			return el;
		}
		this.reset();
		return null;
	}
	
	/**
	 * Retroceder una posici�n en la colecci�n y devolver el elemento actual.
	 * @return unElemento
	 */
	public T prev() {
		if(this.index>=0) {
			T el=this.current();
			this.index -= 1;
			return el;
		}
		this.reset();
		return null;
	}
	
	/////////////////////////////////////////////////
	// La parte interesante, m�todos de colecciones.
	/////////////////////////////////////////////////
	/**
	 * Ejecuta un bloque para cada elemento de la colecci�n.
	 * Admite chaining.
	 * @param unBloque
	 * @return laColeccion
	 */
	public MiColeccion<T> forEach(BloqueExec<T> unBloque) {
		for(T unElemento : this) {
			unBloque.exec(unElemento);
		}
		return this;
	}
	
	/**
	 * Devuelve una nueva colecci�n con los elementos que cumplen la condici�n pasada en el bloque.
	 * @param unBloque
	 * @return unaNuevaColeccionFiltrada
	 */
	public MiColeccion<T> filter(BloqueTest<T> unBloque) {
		MiColeccion<T> ret = new MiColeccion<T>();
		for(T unElemento : this) {
			if(unBloque.test(unElemento)) {
				ret.add(unElemento);
			}
		}
		return ret;
	}
	
	/**
	 * Devuelve una nueva colecci�n con los elementos de la original transformados por la funci�n pasada.
	 * @param unBloque
	 * @return coleccionTransformada
	 */
	public <U> MiColeccion<U> map(BloqueFn<T, U> unBloque) {
		MiColeccion<U> ret=new MiColeccion<U>();
		for(T unElemento : this) {
			ret.add(unBloque.fn(unElemento));
		}
		return ret;
	}
	
	/**
	 * Aplana la colecci�n con el valor inicial que recibe y el bloque.
	 * El equivalente a foldl.
	 * @param unBloque
	 * @param valorInicial
	 * @return resultado
	 */
	public <U> U reduce(BloqueFold<T, U> unBloque, U valorInicial) {
		if(this.size()==0) {
			return valorInicial;
		}
		U ret=valorInicial;
		for(T unElemento : this) {
			ret=unBloque.fn(unElemento, ret);
		}
		return ret;
	}
	
	/**
	 * Foldea la colecci�n con el bloque pasado tomando como valor inicial al primer elemento.
	 * @param unBloque
	 * @return resultado
	 * @throws NoSePuedeFoldearMiColeccionException cuando la colecci�n est� vac�a.
	 */
	public T reduce(BloqueFold<T, T> unBloque) {
		if(this.size()==0) {
			throw new NoSePuedeFoldearMiColeccionException();
		}
		T primero=this.get(0);
		return this.copy().remove(primero).reduce(unBloque, primero);
	}

	/**
	 * Equivalente al foldr.
	 * @param unBloque
	 * @param valorInicial
	 * @return resultado
	 */
	public <U> U reduceRight(BloqueFold<T, U> unBloque, U valorInicial) {
		return this.flip().reduce(unBloque, valorInicial);
	}
	
	/**
	 * Equivalente al foldr, tomando como valor inicial el primer elementod de la colecci�n.
	 * @param unBloque
	 * @return resultado
	 * @throws NoSePuedeFoldearMiColeccionException cuando la colecci�n est� vac�a.
	 */
	public T reduceRight(BloqueFold<T, T> unBloque) {
		return this.flip().reduce(unBloque);
	}
	
	/////////////////////////////////////////////////
	// M�todos �tiles varios.
	/////////////////////////////////////////////////
	/**
	 * Saber si todos los elementos de la colecci�n cumplen la condici�n pasada.
	 * @param unBloque
	 * @return todosCumplen
	 */
	public boolean all(BloqueTest<T> unBloque) {
		return this.filter(unBloque).size()==this.size();
	}
	
	/**
	 * Saber si alg�n elemento de la colecci�n cumple la condici�n pasada.
	 * @param unBloque
	 * @return algunoCumple
	 */
	public boolean any(BloqueTest<T> unBloque) {
		return this.filter(unBloque).size()!=0;
	}

	/**
	 * Devuelve la sumatoria de los elementos de la colecci�n
	 * @return sumatoria
	 */
	@SuppressWarnings("unchecked")
	public Float sum() {
		MiColeccion<Number> copy=(MiColeccion<Number>) this.copy();
		return copy.reduce(new BloqueFold<Number, Float>() {
			@Override
			public Float fn(Number unNro, Float sumatoria) {
				return sumatoria + unNro.floatValue();
			}
		}, new Float(0));
	}
}
